package net.cyndeline.fov.mrpas.defaultImpl

import com.escalatesoft.subcut.inject.BindingModule
import com.escalatesoft.subcut.inject.Injectable
import net.cyndeline.fov.FieldOfView
import net.cyndeline.fov.FieldOfViewParser
import net.cyndeline.fov.MatrixFieldOfView
import net.cyndeline.fov.mrpas.LineParser
import net.cyndeline.fov.mrpas.ObstructedAngles
import net.cyndeline.fov.mrpas.Quadrant
import net.cyndeline.fov.FieldOfViewResult
import net.cyndeline.fov.Visible
import net.cyndeline.fov.VisibilityStatus
import net.cyndeline.fov.Opaque
import net.cyndeline.fov.Hidden

/**
 * Uses the MRPAS algorithm to calculate field of view.
 * 
 * http://roguebasin.roguelikedevelopment.org/index.php?title=Restrictive_Precise_Angle_Shadowcasting
 * 
 */
class DefaultFieldOfViewParser(implicit val bindingModule: BindingModule) extends FieldOfViewParser with Injectable {
  
  def computedFieldOfView(startX: Int, 
		  				  startY: Int, 
		  				  obstacleMap: Array[Array[Int]], 
		  				  radius: Int,
		  				  displayObstacles: Boolean = true): FieldOfView[(Int, Int)] = {
    val result = injectOptional[FieldOfViewResult] getOrElse new MatrixFieldOfView()
    
    /* Impossible to parse a map with 0 width or height. */
    if (obstacleMap.length == 0 || obstacleMap(0).length == 0) {
      return result
    } else {
      result.create(obstacleMap.length, obstacleMap(0).length)
      result.setVisibility(startX, startY, Visible)
    }

    val lineParser = injectOptional[LineParser] getOrElse new DefaultLineParser()
    val quadResult = lineParser.generateQuadrants((startX, startY), obstacleMap, radius)
    val quadrants = quadResult._1 :: quadResult._2 :: quadResult._3  :: quadResult._4 :: Nil
    
    /* For each quadrant, check both sets of lines (one set above the diagonal,
     * one below).
     */
    for (quadrant <- quadrants) {
      val diagonals = quadrant.linesFromHorizontalBorder :: quadrant.linesFromVerticalBorder :: Nil
      
      /* For each diagonal... */
      for (lines <- diagonals) {
        val obstacleRanges = injectOptional[ObstructedAngles] getOrElse new DefaultObstructedAngles()
        var obstaclesFoundToPreviousLine = 0
        var processedLines = 1 // Start excluded by default
        
        /* ...check which positions in each line is visible. */
        for (line <- lines) {
          var obstaclesOnThisLine = 0
          
          /* Which position in the line that is currently being checked,
           * used to calculate the three angles per tile. 
           */
          var currentPositionNr = 0
          
          /* The length is not the actual length of the line object in
           * question, but rather the length that the line would have if
           * the obstacle map were perfectly square.
           */
          val length = processedLines + 1
          val tileRange = 1f / length
          
          for (position <- line.tilePositions) {
            val positionStartAngle = currentPositionNr * tileRange
            val positionMidAngle = positionStartAngle + (tileRange / 2)
            val positionEndAngle = positionStartAngle + tileRange
            
            /* This tile position is only visible if The center angle is clear,
             * along with either the start or stop angle.
             */
            val visibilityStatus = visibleStatus(obstacleRanges, obstaclesFoundToPreviousLine, positionStartAngle, positionMidAngle, positionEndAngle)
            
            if (visibilityStatus == Visible || visibilityStatus == Opaque) {
              result.setVisibility(position._1, position._2, visibilityStatus)
              
              /* Only update obstacle data if the tile is actually in
               * field of vision.
               */
              if (obstacleMap(position._1)(position._2) > 0) {
            	obstaclesOnThisLine += 1
            	line.addObstacle((position._1, position._2))
            	obstacleRanges.addObstructedAngle(positionStartAngle, positionEndAngle)
              }
            }
            
            currentPositionNr += 1
          }
          
          /* Update the obstacle number before moving on to the next line. */
          obstaclesFoundToPreviousLine += obstaclesOnThisLine
          
          processedLines += 1
        }
      }
    }

    result
  }
  
  /**
   * Checks if the angles to a tile position means that the tile is visible,
   * hidden or opaque.
   */
  private def visibleStatus(obstacles: ObstructedAngles, obstNr: Int, start: Float, mid: Float, end: Float): VisibilityStatus = {
    if (!obstacles.isObstructed(mid, obstNr)
                && (!obstacles.isObstructed(start, obstNr)
                    || !obstacles.isObstructed(end, obstNr))) {
      Visible
    } else if (!obstacles.isObstructed(start, obstNr) || !obstacles.isObstructed(end, obstNr)) {
      Opaque
    } else {
      Hidden
    }
  }

}